| 1 | "use client"; |
| 2 | import { useEffect, useState } from "react"; |
| 3 | import { useParams } from "next/navigation"; |
| 4 | import { api, CameraDetail } from "@/lib/api"; |
| 5 | |
| 6 | export default function CameraDetailPage() { |
| 7 | const { id } = useParams<{ id: string }>(); |
| 8 | const [cam, setCam] = useState<CameraDetail | null>(null); |
| 9 | const [transits, setTransits] = useState<{ from: string; to: string; weight: number }[]>([]); |
| 10 | const [err, setErr] = useState<string | null>(null); |
| 11 | |
| 12 | useEffect(() => { |
| 13 | Promise.all([api.cameraDetail(id), api.cameraTransits(id)]) |
| 14 | .then(([c, t]) => { setCam(c); setTransits(t.transitions); }) |
| 15 | .catch((e) => setErr(e.status === 401 ? "Sign in to view camera detail." : "Camera not found.")); |
| 16 | }, [id]); |
| 17 | |
| 18 | if (err) return <div className="badge danger">{err}</div>; |
| 19 | if (!cam) return <div className="muted">Loading…</div>; |
| 20 | |
| 21 | return ( |
| 22 | <div> |
| 23 | <h1>{cam.name || cam.external_camera_id}</h1> |
| 24 | <p className="muted">External ID: {cam.external_camera_id}</p> |
| 25 | <div className="grid"> |
| 26 | <div className="card"><div className="muted">Total reads</div><div className="kpi">{cam.read_count}</div></div> |
| 27 | <div className="card"><div className="muted">Facing</div><div>{cam.direction_facing || "-"}</div></div> |
| 28 | <div className="card"><div className="muted">Location</div><div>{cam.lat != null ? `${cam.lat.toFixed(4)}, ${cam.lon!.toFixed(4)}` : "unknown"}</div></div> |
| 29 | <div className="card"><div className="muted">Address</div><div>{cam.address || "-"}</div></div> |
| 30 | </div> |
| 31 | |
| 32 | <h2>Camera-to-camera transits</h2> |
| 33 | <p className="muted">Observed vehicle movements into and out of this camera (directed, weighted).</p> |
| 34 | <div className="card"> |
| 35 | <table> |
| 36 | <thead><tr><th>From</th><th>To</th><th>Vehicles</th></tr></thead> |
| 37 | <tbody> |
| 38 | {transits.map((t, i) => ( |
| 39 | <tr key={i}> |
| 40 | <td className={t.to === id ? "muted" : ""}>{t.from === id ? "(this camera)" : t.from.slice(0, 8)}</td> |
| 41 | <td className={t.from === id ? "muted" : ""}>{t.to === id ? "(this camera)" : t.to.slice(0, 8)}</td> |
| 42 | <td><span className="badge">{t.weight}</span></td> |
| 43 | </tr> |
| 44 | ))} |
| 45 | {!transits.length && <tr><td colSpan={3} className="muted">No transitions observed.</td></tr>} |
| 46 | </tbody> |
| 47 | </table> |
| 48 | </div> |
| 49 | </div> |
| 50 | ); |
| 51 | } |